

using System;
using System.Collections.Generic;
using System.Linq;
using Lucene.Net.Documents;
using Lucene.Net.Index;
using Lucene.Net.Search;
using Lucene.Net.Store;
using Lucene.Net.Util;
using NopSolutions.NopCommerce.BusinessLogic.Audit;
using NopSolutions.NopCommerce.BusinessLogic.Categories;
using NopSolutions.NopCommerce.BusinessLogic.Manufacturers;
using NopSolutions.NopCommerce.BusinessLogic.Products;
using NopSolutions.NopCommerce.BusinessLogic.Products.Specs;
using NopSolutions.NopCommerce.LuceneHelper.Queries;

namespace NopSolutions.NopCommerce.LuceneHelper
{
    public class SearchManager
    {
        #region Variables/Constructors

        public const int READER_MAX = 25000;

        #endregion

        #region Helper Methods

        private IndexSearcher GetIndexSearch(LuceneIndexType indexType)
        {
            return new IndexSearcher(FSDirectory.Open(DirectoryManager.GetIndex(indexType)), true);
        }

        public IndexSearcher GetProductIndexSearch()
        {
            return GetIndexSearch(LuceneIndexType.Product);
        }

        private IndexSearcher GetSpecificationOptionIndexSearch()
        {
            return GetIndexSearch(LuceneIndexType.SpecificaitonOptions);
        }

        private IndexSearcher GetCategoryIndexSearch()
        {
            return GetIndexSearch(LuceneIndexType.Category);
        }

        private DocIdSet GetDocIdSet(Query query, IndexReader indexReader)
        {
            return new QueryWrapperFilter(query).GetDocIdSet(indexReader);
        }

        private Sort GetSort(ProductsSortBy productsSortBy)
        {
            SortField sortField = null;
            switch (productsSortBy)
            {
                case ProductsSortBy.HighestRated:
                    sortField = new SortField("Rating", SortField.INT, true);
                    break;
                case ProductsSortBy.DisplayOrder:
                    sortField = new SortField("SortOrder", SortField.INT);
                    break;
                case ProductsSortBy.PriceAsc:
                    sortField = new SortField("Price", SortField.FLOAT);
                    break;
                case ProductsSortBy.PriceDesc:
                    sortField = new SortField("Price", SortField.FLOAT, true);
                    break;
                case ProductsSortBy.BrandAsc:
                    sortField = new SortField("BrandName", SortField.STRING);
                    break;
                case ProductsSortBy.BrandDesc:
                    sortField = new SortField("BrandName", SortField.STRING, true);
                    break;
            }
            return sortField != null ? new Sort(sortField) : null;
        }

        public IEnumerable<Document> Search(LuceneIndexType indexType, Query query, Filter filter, Sort sort, int start, int end, out int total)
        {
            var indexSearch = GetIndexSearch(indexType);
            TopDocs docs = sort == null
                               ? indexSearch.Search(query, filter ?? new QueryWrapperFilter(query), end)
                               : indexSearch.Search(query, filter ?? new QueryWrapperFilter(query), end, sort);
            total = docs.totalHits;
            IEnumerable<Document> results = docs.scoreDocs.Skip(start).Select(h => indexSearch.Doc(h.doc)).ToList();
            indexSearch.Close();
            return results;
        }

        #endregion

        #region Search Products

        public IEnumerable<Document> SearchProducts(Query query, Filter filter, ProductsSortBy sortBy, int start, int end,
                                                    out int total)
        {
            var sort = GetSort(sortBy);
            return Search(LuceneIndexType.Product, query, filter, sort, start, end, out total);
        }

        public IEnumerable<Document> SearchProducts(ProductQuery query, Filter filter, ProductsSortBy sortBy, int start, int end,
                                                   out int total)
        {
            query.EnsureQuery();
            return SearchProducts(query.Query, sortBy, start, end, out total);
        }

        public IEnumerable<Document> SearchProducts(ProductQuery query, ProductsSortBy sortBy, int start, int end,
                                                    out int total)
        {
            return SearchProducts(query, null, sortBy, start, end, out total);
        }

        public IEnumerable<Document> SearchProducts(Query query, ProductsSortBy sortBy, int start, int end, out int total)
        {
            return SearchProducts(query, null, sortBy, start, end, out total);
        }

        public IEnumerable<Document> SearchProducts(ProductQuery query)
        {
            query.EnsureQuery();
            int total;
            return Search(LuceneIndexType.Product, query.Query, null, null, 0, Globals.INDEX_READER_MAX, out total);
        }

        public ProductSearchResult SearchProducts(FilterContext filterContext, int start, int end)
        {
            ProductQuery query = new ProductQuery().WithFilterContext(filterContext);
            query.EnsureQuery();

            int total = 0;
            IEnumerable<Document> hits = SearchProducts(query.Query, filterContext.ProductSortBy, start, end, out total);

            var productSearchResults = new ProductSearchResult(start, end, total, query.Query, filterContext,
                                                               hits.ToList(), this);
            return productSearchResults;
        }

        public DocIdSet GetProductDocIdSet(Query query)
        {
            return GetDocIdSet(query, GetProductIndexSearch().GetIndexReader());
        }

        #endregion

        #region Search Categories

        public IEnumerable<Document> SearchCategories(Query query, Filter filter, int start, int end,
                                                      out int total)
        {
            return Search(LuceneIndexType.Category, query, filter, null, start, end, out total);
        }

        public IEnumerable<Document> SearchCategories(Query query, int start, int end, out int total)
        {
            return SearchCategories(query, null, start, end, out total);
        }

        public IEnumerable<Document> SearchCategories(CategoryQuery query)
        {
            query.EnsureQuery();
            int total;
            return SearchCategories(query.Query, 0, Globals.INDEX_READER_MAX, out total);
        }

        public IList<CategoryValue> SearchForCategoriesOnProductQuery(Query query,
                                                                            IEnumerable<Category> expectedCategories)
        {
            var visibleCategories = new List<CategoryValue>();

            IndexSearcher productSearcher = GetProductIndexSearch();
            IndexReader productReader = productSearcher.GetIndexReader();
            DocIdSet productResults = new QueryWrapperFilter(query).GetDocIdSet(productReader);

            foreach (var expectedCategory in expectedCategories)
            {
                var productsDISI = new OpenBitSetDISI(productResults.Iterator(), 25000);
                var termQuery = new TermQuery(new Term("CategoryID", expectedCategory.CategoryId.ToString()));
                var termQueryFilter = new QueryWrapperFilter(termQuery);
                DocIdSetIterator termIterator = termQueryFilter.GetDocIdSet(productReader).Iterator();
                productsDISI.InPlaceAnd(termIterator);
                long total = productsDISI.Cardinality();
                if (total > 0)
                {
                    var visibleCategory = new CategoryValue(expectedCategory, total);
                    visibleCategories.Add(visibleCategory);
                }
            }
            productReader.Close();
            productSearcher.Close();
            return visibleCategories;
        }

        public IList<CategoryValue> SearchForCategoriesOnProductQuery(ProductQuery query,
                                                                            IEnumerable<Category> expectedCategories)
        {
            query.EnsureQuery();
            return SearchForCategoriesOnProductQuery(query.Query, expectedCategories);
        }

        //public IList<CategoryFilterValue> SearchForCategoriesWithWithoutChildren()
        //{
        //    IndexSearcher categorySearch = GetCategoryIndexSearch();
        //    var termQuery = new TermQuery(new Term("HasChildren", "False"));
        //    List<Document> results =
        //        categorySearch.Search(termQuery, 25000).scoreDocs.Select(x => categorySearch.Doc(x.doc)).ToList();
        //    categorySearch.Close();
        //    return results.MapCategoryFilters();
        //}

        #endregion

        #region Search Manufacturers

        public IList<ManufacturerValue> SearchForManufacturersOnProductQuery(Query query,
                                                                           IEnumerable<Manufacturer> expectedManus)
        {
            var visibleManus = new List<ManufacturerValue>();

            IndexSearcher productSearcher = GetProductIndexSearch();
            IndexReader productReader = productSearcher.GetIndexReader();
            DocIdSet productResults = new QueryWrapperFilter(query).GetDocIdSet(productReader);

            foreach (var expectedManu in expectedManus)
            {
                var productsDISI = new OpenBitSetDISI(productResults.Iterator(), 25000);
                var termQuery = new TermQuery(new Term("ManufacturerID", expectedManu.ManufacturerId.ToString()));
                var termQueryFilter = new QueryWrapperFilter(termQuery);
                DocIdSetIterator termIterator = termQueryFilter.GetDocIdSet(productReader).Iterator();
                productsDISI.InPlaceAnd(termIterator);
                long total = productsDISI.Cardinality();
                if (total > 0)
                {
                    var visibleCategory = new ManufacturerValue(expectedManu, total);
                    visibleManus.Add(visibleCategory);
                }
            }
            productReader.Close();
            productSearcher.Close();
            return visibleManus;
        }

        #endregion

        #region Search Specification Options

        public IEnumerable<Document> SearchSpecificationOptions(Query query, Filter filter, int start, int end, out int total)
        {
            var sortField = new SortField("SpecDisplayOrder", SortField.INT);
            var sort = new Sort(sortField);
            return Search(LuceneIndexType.SpecificaitonOptions, query, filter, sort, start, end, out total);
        }

        public IEnumerable<Document> SearchSpecificationOptions(Query query, int start, int end, out int total)
        {
            return SearchSpecificationOptions(query, null, start, end, out total);
        }

        public IEnumerable<Document> SearchSpecificationOptions(SpecificationOptionQuery query, int start, int end, out int total)
        {
            query.EnsureQuery();
            return SearchSpecificationOptions(query.Query, start, end, out total);
        }

        public IEnumerable<SpecificationAttributeOption> SearchSpecificationOptionsInContext(FilterContext context)
        {
            if (context.Manufacturers.Count == 0 && context.Categories.Count == 0)
                return DatabaseHelper.GetAllMappedOptions();

            var query = new SpecificationOptionQuery()
                .WithCategories(context.Categories)
                .WithManufacturers(context.Manufacturers);
            query.EnsureQuery();
            var total = 0;
            return SearchSpecificationOptions(query, 0, READER_MAX, out total).MapSpecificationOptions();
        }

        public IEnumerable<SpecificationAttributeOptionValue> SearchActualSpecificationOptions(IEnumerable<int> specificationIds, Query baseQuery)
        {
            if (specificationIds.Count() == 0)
            {
                return new List<SpecificationAttributeOptionValue>();
            }
            var results = new SpecificationOptionQuery().ForSpecifications(specificationIds.ToList()).Results().MapSpecificationOptions();
            var values = new List<SpecificationAttributeOptionValue>();
            GetFacetCounts(LuceneIndexType.Product, results, baseQuery,
                x => new Term("Spec" + x.SpecificationAttributeId, x.SpecificationAttributeOptionId.ToString()),
                (x, y) => values.Add(new SpecificationAttributeOptionValue(x, y)));
            return values;
        }

        #endregion

        #region Search Specification Attributes

        public IEnumerable<SpecificationAttribute> SearchSpecificationAttributesInContext(FilterContext context, bool excludeAllreadyFilteredAttributes)
        {
            if (context.Categories.Count == 0 && context.Manufacturers.Count == 0)
            {
                return DatabaseHelper.GetAllMappedAttributes();
            }

            var query = new SpecificationAttributeQuery()
                .WithCategories(context.Categories)
                .WithManufacturers(context.Manufacturers);

            if (excludeAllreadyFilteredAttributes)
            {
                query =
                    query.ExcludeSpecificationAttributes(
                        context.Filters.Select(x => x.SpecificationAttributeId).Distinct());
            }

            return query.Results(this).MapSpecificationAttribute();
        }

        public IEnumerable<SpecificationAttributeValue> SearchActualSpecificationAttributesInContext(FilterContext context, Query baseQuery)
        {
            var specAttributes = SearchSpecificationAttributesInContext(context, true);
            var values = new List<SpecificationAttributeValue>();
            GetFacetCounts(LuceneIndexType.Product, specAttributes, baseQuery,
                x => new Term("Specification", x.SpecificationAttributeId.ToString()),
                (x, y) => values.Add(new SpecificationAttributeValue(x, y)));
            return values.OrderByDescending(x => x.Total);
        }

        #endregion

        #region Facets

        public IList<Facet> GetFacetsOnProductQuery(FilterContext filterContext, Query query)
        {
            var attributes = SearchActualSpecificationAttributesInContext(filterContext, query).Take(10).ToList();
            var options = SearchActualSpecificationOptions(attributes.Select(x => x.SpecificationAttributeId), query).OrderByDescending(x => x.Total);
            return GroupToFacets(attributes, options);
        }

        private IList<Facet> GroupToFacets(IEnumerable<SpecificationAttributeValue> attributes, IEnumerable<SpecificationAttributeOptionValue> options)
        {
            var facets = new List<Facet>();
            foreach (var option in options)
            {
                var o = option;
                var facet =
                    facets.SingleOrDefault(
                        x => x.Attribute.SpecificationAttributeId == o.SpecificationAttributeId);
                if (facet == null)
                {
                    var attribute =
                        attributes.SingleOrDefault(x => x.SpecificationAttributeId == o.SpecificationAttributeId);
                    if (attribute != null)
                    {
                        facet = new Facet(attribute);
                        facets.Add(facet);
                    }

                }
                if (facet == null)
                {
                    LogManager.InsertLog(LogTypeEnum.Unknown, "Lucene error in GroupToAttributes()",
                                             new Exception("Lucene error in GroupToAttributes()"));
                }
                else
                {
                    facet.Options.Add(o);
                }
            }
            foreach (var facet in facets)
            {
                facet.SortOptions();
            }
            return facets.OrderByDescending(x => x.Attribute.Total).ToList();
        }

        #endregion

        #region Faceting

        //public IList<SpecificationAttributeOptionValue> GetActualFacets(Query baseQuery, IEnumerable<SpecificationAttributeOption> expectedFacets)
        //{
        //    var actualFacetValues = new List<SpecificationAttributeOptionValue>();
        //    var productSearcher = GetProductIndexSearch();
        //    var productReader = productSearcher.GetIndexReader();
        //    var productResults = new CachingWrapperFilter(new QueryWrapperFilter(baseQuery)).GetDocIdSet(productReader);

        //    foreach (SpecificationAttributeOption expectedFacet in expectedFacets)
        //    {
        //        var termQuery =
        //            new TermQuery(new Term("Spec" + expectedFacet.SpecificationAttributeId,
        //                                   expectedFacet.SpecificationAttributeOptionId.ToString()));
        //        var filter = new CachingWrapperFilter(new QueryWrapperFilter(termQuery));
        //        DocIdSet filterBitSet = filter.GetDocIdSet(productReader);
        //        var total = GetFacetHitCount(productResults, filterBitSet);
        //        if (total > 0)
        //        {
        //            var specificationAttributeOptionValue = new SpecificationAttributeOptionValue(expectedFacet, total);
        //            actualFacetValues.Add(specificationAttributeOptionValue);
        //        }
        //    }

        //    productReader.Close();
        //    productSearcher.Close();

        //    return actualFacetValues;
        //}

        public void GetFacetCounts<T>(LuceneIndexType indexType, IEnumerable<T> objects, Query baseQuery, Func<T, Term> buildTerm, Action<T, long> setter)
        {
            if (objects.Count() > 0)
            {
                var indexSearcher = GetIndexSearch(indexType);
                var indexReader = indexSearcher.GetIndexReader();
                var baseBitSet = new CachingWrapperFilter(new QueryWrapperFilter(baseQuery)).GetDocIdSet(indexReader);

                foreach (var expectedFacet in objects)
                {
                    var termQuery = new TermQuery(buildTerm.Invoke(expectedFacet));
                    var filter = new CachingWrapperFilter(new QueryWrapperFilter(termQuery));
                    DocIdSet filterBitSet = filter.GetDocIdSet(indexReader);
                    var total = GetFacetHitCount(baseBitSet, filterBitSet);
                    if (total > 0)
                    {
                        setter.Invoke(expectedFacet, total);
                    }
                }

                indexReader.Close();
                indexSearcher.Close();
            }
        }

        private long GetFacetHitCount(DocIdSet baseBitSet, DocIdSet filterBitSet)
        {
            ((OpenBitSet)filterBitSet).And((OpenBitSet)baseBitSet);
            return ((OpenBitSet)filterBitSet).Cardinality();
        }

        #endregion

        #region GetPriceRanges

        private IList<PriceRange> GetPriceRanges(decimal from, decimal to)
        {
            var priceRanges = new List<PriceRange>();

            decimal thisLow = from;
            decimal thisHigh = 2 * thisLow - .01M;

            if (thisLow == 0)
                thisHigh = 1;

            int loopCatch = 1;

            while (thisLow <= to && loopCatch < 100)
            {
                priceRanges.Add(new PriceRange { From = thisLow, To = thisHigh });
                thisLow = thisHigh + .01M;
                thisHigh = 2 * thisLow - .01M;
                loopCatch += 1;
            }
            return priceRanges;
        }

        public IList<PriceRange> GetPriceRanges(Query query)
        {
            try
            {
                var searchManager = new SearchManager();
                int total = 0;
                var productSearch = searchManager.SearchProducts(query, ProductsSortBy.PriceAsc, 0, 1,
                                                                                   out total);
                if (total > 0)
                {
                    decimal min = productSearch.MapProducts(false)[0].MinimalPriceProductVariant.Price;
                    productSearch = searchManager.SearchProducts(query, ProductsSortBy.PriceDesc, 0, 1, out total);
                    decimal max = productSearch.MapProducts(false)[0].MinimalPriceProductVariant.Price;
                    return GetPriceRanges(min, max);
                }
            }
            catch (Exception)
            {
            }
            return new List<PriceRange>();
        }

        public IList<PriceRangeValue> GetPriceRanges(Query query, IEnumerable<PriceRange> ranges)
        {
            IndexReader productReader = GetProductIndexSearch().GetIndexReader();
            DocIdSet productIdSet = new QueryWrapperFilter(query).GetDocIdSet(productReader);
            var rangeValues = new List<PriceRangeValue>();
            foreach (PriceRange range in ranges)
            {
                if (range.From != null && range.To != null)
                {
                    var productsDISI = new OpenBitSetDISI(productIdSet.Iterator(), 25000);
                    var from = ((float)range.From.Value);
                    var to = ((float)range.To.Value);
                    NumericRangeFilter rangeFilter = NumericRangeFilter.NewFloatRange("Price", from, to, true, true);
                    DocIdSetIterator rangeIterator = rangeFilter.GetDocIdSet(productReader).Iterator();
                    productsDISI.InPlaceAnd(rangeIterator);
                    long total = productsDISI.Cardinality();
                    if (total > 0)
                    {
                        rangeValues.Add(new PriceRangeValue(total) { From = range.From, To = range.To });
                    }
                }
            }
            productReader.Close();
            return rangeValues;
        }

        #endregion
    }
}